/*
 * Copyright (C) 2012 Tomas Shestakov. <http://code.google.com/p/jmmo/>
 */

package jmmo.engine.multifacetedactor

import org.scalatest.{BeforeAndAfterAll, WordSpec}
import akka.event.EventStream
import akka.testkit.{TestActorRef, ImplicitSender, TestKit}
import org.scalatest.matchers.ShouldMatchers
import akka.actor._

/**
 * Tests actor abilities to subscribe and unsubscribe to [[akka.event.EventStream]].
 * @author Tomas Shestakov
 * @param _system actor system for testing
 * @param creator a function which returns new instance of [[jmmo.engine.multifacetedactor.MultifacetedActor]]
 */
class ActorChildrenSpec(_system: ActorSystem, creator: => MultifacetedActor with ActorChildren, shutdown: Boolean = false)
                extends TestKit(_system) with ShouldMatchers with ImplicitSender with WordSpec with BeforeAndAfterAll {

  def this() = this(ActorSystem("TestSpec"), new MultifacetedActor with ActorChildren {}, true)

  override def afterAll {
    if (shutdown) system.shutdown()
  }

  //add or remove child actors
  val ownChildrenRole = new ActorRole {
    var watcher: ActorRef = _
    def id = 'ownChildrenRole
    def receive = {
      case 'addChild  => sender ! context.actorOf(Props.empty)
      case child: ActorRef =>
        if (context.children.find(_ == child).isDefined) {
          watcher = sender
          context.watch(child)
          context.stop(child)
        }
      case msg @ Terminated(_) => watcher ! Some(msg)
    }
  }

  val subscriber = new ActorRole {
    def id = 'subscriber
    def receive = {
      case 'subscribe => context.system.eventStream.subscribe(testActor, classOf[ChildEvent])
      case 'unsubscribe => context.system.eventStream.unsubscribe(testActor, classOf[ChildEvent])
    }
  }

  val parent = TestActorRef[MultifacetedActor with ActorChildren](Props({
      val newActor = creator
      newActor.addRole(ownChildrenRole)
      newActor.addRole(subscriber)
      newActor
  }))

  val someActor = system.actorOf(Props.empty)

  override val nestedSuites = {
    List(new MultifacetedActorSpec(system, creator))
  }

  "A ActorSubscriber" should {

    "fit MultifacetedActorSpec" in {}

    "on new actor methods `ownChildren`, `alienChildren` and `allChildren` returns empty Iterable" in {
      parent.underlyingActor.ownChildren should be ('empty)
      parent.underlyingActor.alienChildren should be ('empty)
      parent.underlyingActor.allChildren should be ('empty)
    }

    "actors created from actor context must be in `ownChildrean` and `allChildren` Iterables" in {
      parent ! 'addChild
      val child = expectMsgType[ActorRef]
      parent.underlyingActor.ownChildren.toList should be (List(child))
      parent.underlyingActor.allChildren.toList should be (List(child))
    }

    "actors stoped from actor context must be absent in `ownChildrean` and `allChildren` Iterables" in {
      val child = parent.underlyingActor.ownChildren.iterator.next()
      parent ! child
      expectMsgType[Option[Terminated]]
      parent.underlyingActor.ownChildren should be ('empty)
      parent.underlyingActor.allChildren should be ('empty)
    }

    "provide `addAlienChild` method" which {

      "add ActorRef to `alienChidlren` and `allChildren` Iterables" in {
        parent.underlyingActor.addAlienChild(someActor)
        parent.underlyingActor.alienChildren.toList should be (List(someActor))
        parent.underlyingActor.allChildren.toList should be (List(someActor))
      }

      "not add more than one same ActorRef to `alienChidlren` and `allChildren` Iterables" in {
        parent.underlyingActor.addAlienChild(someActor)
        parent.underlyingActor.alienChildren.toList should be (List(someActor))
        parent.underlyingActor.allChildren.toList should be (List(someActor))
      }
    }

    "provide `removeAlienChild` method" which {

      "do nothing on remove ActorRef which does not exists in `alienChidlren` Iterable" in {
        parent.underlyingActor.removeAlienChild(system.actorOf(Props.empty))
        parent.underlyingActor.alienChildren.toList should be (List(someActor))
        parent.underlyingActor.allChildren.toList should be (List(someActor))
      }

      "remove ActorRef from `alienChidlren` and `allChildren` Iterables" in {
        parent.underlyingActor.removeAlienChild(someActor)
        parent.underlyingActor.alienChildren should be ('empty)
        parent.underlyingActor.allChildren should be ('empty)
      }
   }

    "handle `AddChildRequest` message by call `addAlienChild` method and send `ChildAdded` to subscribers on `EventBus`" in {
      parent ! 'subscribe
      parent ! AddChildRequest(someActor)
      expectMsg(ChildAdded(someActor))
      parent.underlyingActor.alienChildren.toList should be (List(someActor))
      parent.underlyingActor.allChildren.toList should be (List(someActor))
    }

    "ignore `AddChildRequest` messages with ActorRef already exists in alienChildren" in {
      parent ! AddChildRequest(someActor)
      expectNoMsg()
      parent.underlyingActor.alienChildren.toList should be (List(someActor))
      parent.underlyingActor.allChildren.toList should be (List(someActor))
    }

    "handle `GetChildrenRequest` message by send back `GetChildrenResponse` message with content of `allChildren`" in {
      parent ! GetChildrenRequest
      val childrenResponse = expectMsgType[GetChildrenResponse]
      childrenResponse.children.toList should be (List(someActor))
    }

    "ignore `RemoveChildRequest` messages with ActorRef does not exists in alienChildren" in {
      parent ! RemoveChildRequest(system.actorOf(Props.empty))
      expectNoMsg()
      parent.underlyingActor.alienChildren.toList should be (List(someActor))
      parent.underlyingActor.allChildren.toList should be (List(someActor))
    }

    "handle `RemoveChildRequest` messages by remove ActorRef from `alienChidlren` and `allChildren` and send `ChildRemoved` to subscribers on `EventBus`" in {
      parent ! RemoveChildRequest(someActor)
      expectMsg(ChildRemoved(someActor))
      parent.underlyingActor.alienChildren should be ('empty)
      parent.underlyingActor.allChildren should be ('empty)
    }
  }

}
